# python-resmon - Python tools for Resmon monitoring and the Circonus API
# Copyright (c) 2010 Crown coypright
# 
# This file is part of python-resmon.
# 
# python-resmon is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# 
# python-resmon is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with python-resmon.  If not, see <http://www.gnu.org/licenses/>.

import re
import copy
from ply import yacc

from lexer import tokens

# Abstract syntax tree classes

class RuleParserError(Exception):
    """There was a problem parsing the manifest"""


class UndefinedVariable(Exception):
    """A variable was referenced that was not defined."""


class Scope(object):
    """A stack that contains variable bindings.
    
    Because this is a declarative language, we take a scoping model of
    "inheritance with overriding". So the using class gets to override any
    variables in the class that defines it.

    When we push a scope, therefore, we are pushing at the bottom of the stack,
    just above the global scope.
    """
    def __init__(self, global_scope={}, stack=[]):
        self.global_scope = global_scope
        self.scopes = stack

    def push_top(self, scope):
        return Scope(self.global_scope, self.scopes + [scope])

    def push_bottom(self, scope):
        return Scope(self.global_scope, [scope] + self.scopes)

    def __getitem__(self, key):
        try:
            for level in self.scopes:
                if key in level:
                    return level[key]
            return self.global_scope[key]
        except KeyError:
            raise UndefinedVariable(key, self)

    def keys(self):
        ks = set()
        for level in self.scopes:
            ks = ks.union(set(level.keys()))
        return ks

    def dict(self):
        d = {}
        for k in self.keys():
            d[k] = self[k]
        return d

    def __repr__(self):
        return '<Scope %r>' % self.dict()

    def resolve(self, value):
        if isinstance(value, str):
            return re.sub(r'\$(\w+)', lambda mo: self[mo.group(1)], value)
        return value


class Expression(object):
    """At the moment an expression is just a constant, variable or variable substitution"""
    def __init__(self, value):
        self.value = value

    def evaluate(self, scope):
        raise NotImplemented()


class String(Expression):
    def evaluate(self, scope):
        def lookup(mo):
            return scope[mo.group(1)]

        return re.sub(r'\${(\w+)}', lookup, re.sub(r'\$(\w+)', lookup, self.value))


class Constant(Expression):
    def evaluate(self, scope):
        return self.value


class VariableRef(Expression):
    def evaluate(self, scope):
        return scope[self.value]


class Rule(object):
    def __init__(self, severity, condition, value, wait=0):
        self.severity = severity
        self.condition = condition
        self.value = value
        self.wait = wait

    def bind(self, scope):
        return Rule(self.severity.evaluate(scope), self.condition, self.value.evaluate(scope), self.wait)

    def circonus_rule(self):
        CONDITIONS = {
            '>': 'max value',
            '<': 'min value',
            '==': 'match',
            '!=': 'does not match',
        }
        rule = {
            'severity': self.severity,
            'criteria': CONDITIONS[self.condition],
            'value': self.value,
            'wait': self.wait
        }
        if self.value == 0:
            if self.condition == '>':
                rule['value'] = 0.000001
            else:
                rule['value'] = -0.000001
        return rule

    def __repr__(self):
        return 'Rule(%r, %r, %r, %r)' % (self.severity, self.condition, self.value, self.wait)


class Metric(object):
    def __init__(self, name, alerts=[], human_name=None, format=None):
        self.name = name
        self.alerts = alerts
        self.human_name = human_name
        self.format = format

    def get_classes(self):
        return []

    def bind(self, scope):
        bound_alerts = [a.bind(scope) for a in self.alerts]
        kwargs = {}
        bname = self.name.evaluate(scope)

        if self.human_name is not None:
            kwargs['human_name'] = self.human_name.evaluate(scope)

        if self.format is not None:
            kwargs['format'] = self.format.evaluate(scope)

        return Metric(bname, bound_alerts, **kwargs)

    def get_absence_severity(self):
        return min([5] + [r.severity for r in self.alerts])

    def get_human_name(self):
        return self.human_name or self.name

    def get_format(self):
        return self.format or '%s'

    def get_metrics(self, scope):
        return [self.bind(scope)]

    def get_rules(self):
        rs = [copy.copy(r) for r in self.alerts]
        for r in rs:
            if self.format is not None:
                r.human_value = self.format % r.value
            else:
                r.human_value = '%s' % r.value
        return rs

    def __repr__(self):
        return 'metric(%r, alerts=%r, human_name=%r, format=%r)' % (self.name, self.alerts, self.human_name, self.format)


class Reference(object):
    def __init__(self, using):
        self.using = using
        self.args = []
        self.ref = None # unresolved

    def resolve(self, obj):
        self.ref = obj

    def get_classes(self):
        return self.ref.get_classes()

    def get_metrics(self, scope):
        return self.ref.get_metrics(scope)


class UseClass(Reference):
    def __repr__(self):
        return 'UseClass(%r)' % (self.using)

    def resolve(self, obj):
        if hasattr(obj, 'params'):
            raise UseError("%s is a define; cannot be used as a class" % (self.using))
        return super(UseClass, self).resolve(obj)


class UseError(Exception):
    """There is a problem resolving a reference"""


class UseDefine(Reference):
    def __init__(self, using, args=[]):
        super(UseDefine, self).__init__(using)
        self.args = args

    def resolve(self, obj):
        try:
            params = obj.params
        except AttributeError:
            raise UseError("%s does not take arguments (got %d arguments)" % (self.using, len(self.args)))

        if len(self.args) != len(params):
            raise UseError("Invalid number of arguments to %s (expected %d, got %d)" % (self.using, len(params), len(self.args)))
        self.ref = obj
        self.bindings = dict(zip(self.ref.params, self.args))

    def get_metrics(self, scope):
        # Pass by value - resolve the arguments we pass
        b = {}
        for k, v in self.bindings.items():
            b[k] = v.evaluate(scope)

        # Evaluate the reference with this scope
        inner = scope.push_top(b)
        ms = self.ref.get_metrics(inner)
        return ms

    def __repr__(self):
        return 'UseDefine(%r, args=%r)' % (self.using, self.args)



class Block(object):
    """A block is a name group of metrics and references"""
    def __init__(self, definitions=[], scope={}):
        self.definitions = definitions
        self.scope = scope

    def get_classes(self):
        cs = []
        for d in self.definitions:
            cs += d.get_classes()
        return cs

    def get_metrics(self, scope):
        """Collect metrics, ensuring that metrics with the same name supercede each other
        
        - Order of use references matters - subsequent uses override previous ones
        - And locally-defined metrics always supercede referenced ones
        """
        inner = scope.push_bottom(self.scope)
        sub_metrics = {}
        local_metrics = {}

        for d in self.definitions:
            if isinstance(d, Block):
                for m in d.get_metrics(inner):
                    sub_metrics[m.name] = m
            else:
                for m in d.get_metrics(inner):
                    local_metrics[m.name] = m

        sub_metrics.update(local_metrics)
        return sub_metrics.values()


class Define(Block):
    """A Define is a block that takes parameters."""
    def __init__(self, symbol, params=[], definitions=[]):
        self.symbol = symbol
        self.params = params
        super(Define, self).__init__(definitions)

    def __repr__(self):
        return 'Define(%r, params=%r, definitions=%r)' % (self.symbol, self.params, self.definitions)


class RClass(Block):
    """An RClass (Rule Class) is a block of that can be imported
    into other classes and nodes.
    """
    def __init__(self, symbol, definitions=[]):
        self.symbol = symbol
        super(RClass, self).__init__(definitions)

    def get_classes(self):
        cs = [self.symbol]
        for d in self.definitions:
            cs += d.get_classes()
        return cs

    def __repr__(self):
        return 'RClass(%r, definitions=%r)' % (self.symbol, self.definitions)


class Node(Block):
    """A node is a block that corresponds to a host."""
    def __init__(self, hostname, definitions=[]):
        self.hostname = hostname
        super(Node, self).__init__(definitions, {'hostname': self.hostname})

    def __hash__(self):
        return hash('__Node_' + self.hostname)

    def get_metrics(self, scope=None):
        return super(Node, self).get_metrics(scope or Scope())

    def get_absence_severity(self):
        return min([5] + [m.get_absence_severity() for m in self.get_metrics()])

    def __repr__(self):
        return 'Node(%r, definitions=%r)' % (self.hostname, self.definitions)


def p_ruleset(p):
    """ruleset : rule
               | ruleset rule
    """
    if len(p) > 2:
        p[0] = p[1] + [p[2]]
    else:
        p[0] = [p[1]]


def p_rule_base_class(p):
    """rule : CLASS IDENTIFIER LBRACE definition_list RBRACE
            | CLASS IDENTIFIER LBRACE definition_list RBRACE SEMICOLON
    """
    p[0] = RClass(p[2], definitions=p[4]) 


def p_rule_inherited_class(p):
    """rule : CLASS IDENTIFIER INHERITS IDENTIFIER LBRACE definition_list RBRACE
            | CLASS IDENTIFIER INHERITS IDENTIFIER LBRACE definition_list RBRACE SEMICOLON
    """
    p[0] = RClass(p[2], definitions=[UseClass(p[4])] + p[6]) 


def p_definition_list_empty(p):
    """definition_list : """
    p[0] = []


def p_definition_list(p):
    """definition_list : definition
                       | definition_list definition
    """
    if len(p) > 2:
        p[0] = p[1] + [p[2]]
    else:
        p[0] = [p[1]]


def p_comparison(p):
    """comparison : LT
                  | GT
                  | NE
                  | EQ
    """
    p[0] = p[1]


def p_expr_constant(p):
    """expr : INTEGER
            | DECIMAL
    """
    p[0] = Constant(p[1])

def p_expr_string(p):
    """expr : STRING
    """
    p[0] = String(p[1])

def p_expr_variable(p):
    """expr : VARIABLE
    """
    p[0] = VariableRef(p[1])


def p_definition_metric_formatted(p):
    """definition : METRIC expr namebinding format alertdef SEMICOLON"""
    p[0] = Metric(p[2], p[5], p[3], p[4]);


def p_alertdef_empty(p):
    """alertdef : """
    p[0] = []

def p_alertdef(p):
    """alertdef : LBRACE alert_list RBRACE """
    p[0] = p[2]


def p_alert_list_empty(p):
    """alert_list : """
    p[0] = []


def p_alert_list(p):
    """alert_list : alert
                  | alert_list alert
    """
    if len(p) > 2:
        p[0] = p[1] + [p[2]]
    else:
        p[0] = [p[1]]


def p_format_empty(p):
    """format : """

def p_format(p):
    """format : FORMAT expr"""
    p[0] = p[2]

def p_namebinding_empty(p):
    """namebinding : """

def p_namebinding(p):
    """namebinding : AS expr"""
    p[0] = p[2]


def p_definition_alert(p):
    """alert : ALERT AT SEVERITY expr IF VALUE comparison expr wait SEMICOLON"""
    p[0] = Rule(p[4], p[7], p[8], p[9])


def p_wait_empty(p):
    """wait : """
    p[0] = 0

def p_wait(p):
    """wait : FOR TIMESPEC"""
    p[0] = p[2]


def p_definition_useclass(p):
    """definition : USE IDENTIFIER SEMICOLON"""
    p[0] = UseClass(p[2])


def p_rule_define(p):
    """rule : DEFINE IDENTIFIER LPAREN param_list RPAREN LBRACE definition_list RBRACE""" 
    p[0] = Define(p[2], p[4], p[7])


def p_rule_define_noargs(p):
    """rule : DEFINE IDENTIFIER LPAREN RPAREN LBRACE definition_list RBRACE""" 
    p[0] = Define(p[2], [], p[6])


def p_param_list(p):
    """param_list : VARIABLE
                  | param_list COMMA VARIABLE"""
    if len(p) > 2:
        p[0] = p[1] + [p[3]]
    else:
        p[0] = [p[1]]


def p_definition_def(p):
    """definition : USE IDENTIFIER LPAREN arg_list RPAREN SEMICOLON"""
    p[0] = UseDefine(p[2], p[4])


def p_arg_list(p):
    """arg_list : expr
                | arg_list COMMA expr"""
    if len(p) > 2:
        p[0] = p[1] + [p[3]]
    else:
        p[0] = [p[1]]


def p_rule_base_node(p):
    """rule : NODE hostname LBRACE definition_list RBRACE"""
    p[0] = Node(p[2], definitions=p[4]) 


def p_rule_noding(p):
    """rule : NODE hostname USING IDENTIFIER LBRACE definition_list RBRACE"""
    p[0] = Node(p[2], definitions=[UseClass(p[4])] + p[6])


def p_rule_hostname(p):
    """hostname : HOSTNAME
                | IDENTIFIER
                | STRING
    """
    p[0] = p[1]

parser = yacc.yacc()
